跳到主要内容

HTTP 协议学习

HTTP 概述

HTTP:Hyper Text Transfer Protocol(超文本协议,超是指:超链接)用于从服务器传输超文本到本地浏览器的传输协议

HTTP 协议是基于 TCP/IP 的应用层协议 ,它不关心数据传输的细节,主要是用来规定客户端和服务端的传输格式,最初是用来向客户端传输 HTML 页面的内容。默认端口是 80

HTTP 是基于请求与响应模式的,无状态的应用层的协议

为什么基于 TCP 的 HTTP 是无状态呢?

这个无状态,即每一次请求之间是没有联系的,都是独立的,因此服务器不知道请求两次之间是否是同一个用户。

在一次TCP连接之中,每一次的数据交换都和上一次紧密相关的,TCP报文存在ACK字段用于确认上次接收的报文,并且TCP在建立连接时交换了很多的连接配置信息,例如收、发缓存大小,报文序号等。所以,每一次TCP数据交换两方都是能够确切知道对方的信息的。 但是为什么 HTTP 协议确实无状态的呢?

因为 HTTP 是短连接,即每次 “请求-响应” 都是一次 TCP 连接。比如用户一次请求就是一次 TCP 连接,服务器响应结束后断开连接。而每次 TCP 连接是没有关联的,因此 HTTP 是无状态的。如果想要使得每次 TCP 连接之间有关联,服务器和浏览器就得存储相关的信息,这个就是 Cookie 和 Session 的作用。

HTTP 1.1 的长连接

但是又出现新的问题,HTTP 1.1 后支持了 keep-alive,即长连接,那么每次 HTTP 请求还是无状态吗?

答案是肯定的。使用请求头的: Connection: keep-alive

虽然 HTTP 1.1 为了效率,支持了 keep-alive,但是 这个 keep-alive 指的是在一定时间内复用同一个 TCP 连接,而不是 HTTP 连接,每一次请求之间依然没有什么关系(一个 Request 对应一个 Response),都是独立的,因此服务器不知道请求两次之间是否是同一个用户。所以依旧是无状态

例如设置为 10秒内的 HTTP 请求是使用同一个 TCP 连接,10秒后又重新进行连接(这个时间可以通过设置 HTTP 进程的配置文件来修改)。但是如果这个时间太长的也不行,否则的话,TCP连接将会越来越多,直到把服务器的 TCP 连接数量撑爆到上限为止。

提示

“HTTP连接” 这个词就不应该出现,它只是一个应用层的协议,根本就没有所谓的连接这一说,实际上,说 HTTP 请求和 HTTP 响应会更准确一些,而 HTTP 请求和 HTTP 响应,都是通过 TCP 连接这个通道来回传输的。

长轮询和短轮询

因为 HTTP 协议通信只能由客户端发起,所以像聊天室功能,如果想要知道是否有新消息需要使用轮询的方式,不断的询问服务端是否有新消息。所以这里就有两种模式:长轮询和短轮询

短轮询模式:最简单的一种方式,就是用 JS 写个死循环,不停的去请求服务器当前是否有新消息,然后刷新到这个页面当中,这其实就是所谓的短轮询。

客户端:啦啦啦,有没有新信息(Request)
服务端:没有(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:没有。。(Response)
客户端:啦啦啦,有没有新信息(Request)
服务端:你好烦啊,没有啊。。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:好啦好啦,有啦给你。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:。。。。。没。。。。没。。。没有(Response)

这种方式有明显的坏处,那就是很浪费服务器和客户端的资源。客户端还好点,现在电脑配置高了,不停的请求还不至于把用户的电脑整死,但是服务器就很蛋疼了。如果有 1000 个人在聊天,那就是说会有 1000 个客户端不停的去请求服务器获取是否有新消息,这显然是不合理的。

长轮询模式:长轮询这个时候就出现了,长轮询和短轮询最大的区别是,短轮询去服务端查询的时候,不管是否有新消息,服务器都立即返回结果了。而长轮询则不是,在长轮询中,服务器如果检测到没有新消息的话,将会把当前请求挂起一段时间(这个时间也叫作超时时间,一般是几十秒)。在这个时间里,服务器会去检查是否有新消息,检测到有消息就立即返回,否则就一直等到超时为止。

而对于客户端来说,不管是长轮询还是短轮询,客户端的动作都是一样的,就是不停的去请求,不同的是服务端,短轮询情况下服务端每次请求不管有没有变化都会立即返回结果,而长轮询情况下,如果有变化才会立即返回结果,而没有变化的话,则不会再立即给客户端返回结果,直到超时为止。

这样一来,客户端的请求次数将会大量减少(这也就意味着节省了网络流量,毕竟每次发请求,都会占用客户端的上传流量和服务端的下载流量),而且也解决了服务端一直疲于接受请求的窘境。

但是长轮询也是有坏处的,因为把请求挂起同样会导致资源的浪费,假设还是有 1000 个人在聊天,那就很有可能服务器这边挂着 1000 个线程,在不停检测消息,这依然是有问题的。

因此,从这里可以看出,不管是长轮询还是短轮询,都不太适用于客户端数量太多的情况,因为每个服务器所能承载的 TCP 连接数是有上限的,这种轮询很容易把连接数顶满。

HTTP 各版本区别

HTTP 1.0

HTTP 1.0 规定浏览器与服务器只保持短暂的连接,浏览器的 每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求,所以 HTTP 1.0 最大的问题就是 连接无法复用 且 每次请求都需要等待上一次的请求完成后才能再次请求

HTTP1.1

HTTP 1.1支持持久连接,在 一个TCP连接上可以传送多个HTTP请求和响应,一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输;

HTTP 1.1 还允许客户端 不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容

将 Host 作为必选项是 HTTP/1.1 的重要改进,这使得服务器能够充分利用虚拟主机托管技术

HTTP 请求报文

一个 HTTP 请求报文由四个部分组成:请求行、请求头部、空行、请求数据。

注意:只有 POST 请求才有 Body

请求行

请求行由 请求方法字段URL字段HTTP协议版本字段 3个字段组成,它们用空格分隔。比如

GET /data/info.html HTTP/1.1
  • 方法字段就是 HTTP 使用的请求方法,比如常见的 GET/POST
  • 其中 HTTP 协议版本有三种:HTTP1.0/HTTP1.1/HTTP2.0

主要的请求方式

  • GET 查询
  • POST 添加
  • PUT 修改
  • DELETE 删除

请求头部

请求头例:

Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: no-cache
Connection: keep-alive
Host: alsritter.icu
Pragma: no-cache
Referer: http://alsritter.icu/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36

常用的请求头:客户端告诉服务端的信息

User-Agent:浏览器告诉服务器,当前使用的浏览器版本信息(可以在服务器端取得这个信息来解决不同设备的兼容性问题)

Accept:希望得到的响应格式(比如文本,图片等) Accept-Language:可以支持的语言环境 Accept-Encoding:可以支持的压缩格式

Referer:告诉服务器,当前请求从哪里来 作用: 1、防盗链 2、统计工作

Connection:表示连接是否可被复用(就是上面说的 HTTP1.1 的新特性) keep-alive 表示可复用

请求空行

它的作用是通过一个空行,告诉服务器请求头部到此为止。

请求体

就是封装 POST 请求参数的

若方法字段是 GET,则此项为空,没有数据

若方法字段是 POST,则通常来说此处放置的就是要提交的数据

比如要使用 POST 方法提交一个表单,其中有 user 字段中数据为 “admin”,password 字段为 123456,那么这里的请求数据就user=admin&password=123456,使用 & 来连接各个字段。

HTTP 响应报文

请求消息是客户端发送给服务器端的消息,而响应消息则是服务器回传给客户端的消息

HTTP响应报文也由三部分组成:响应行、响应头、响应体

响应行

响应行一般由协议版本、状态码及其描述组成 比如 HTTP/1.1 200 OK 其中协议版本 HTTP/1.1 或者 HTTP/1.0,200就是它的状态码,OK则为它的描述。

常见状态码:

100~199:表示成功接收请求,要求客户端继续提交下一次请求才能完成整个处理过程。

200~299:表示成功接收请求并已完成整个处理过程。常用200

300~399:重定向 例如:请求的资源已经移动一个新地址。(需要在响应头里写 location 告诉新地址) 常用 302:拿到服务器传来的新地址进行请求 307和304:服务器资源没有变化时,检测到请求的这个资源本地有缓存,让客户端读取本地的缓存

400~499:客户端的请求有错误。 常用 404:意味着你请求的资源在web服务器中没有 403:服务器拒绝访问,权限不够 405:请求方式没有对应的执行方法,例如客户端发起 GET 请求,服务端只有 doPost 方法

500~599:服务器端出现错误,常用500

响应头

响应头用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。

设置 HTTP 响应头往往和状态码结合起来

例如,有好几个表示 “文档位置已经改变” 的状态代码都伴随着一个 Location 头,而40(Unauthorized)状态代码则必须伴随一个 WWW-Authenticate 头。然而,即使在没有设置特殊含义的状态代码时,指定应答头也是很用的。应答头可以用来完成:设置 Cookie,指定修改日期,指示浏览器按照指定的间隔刷新页面,等等许多其他任务。

常见的响应头

Allow:服务器支持哪些请求方法(如 GET、POST 等)。

Content-Type:表示后面的文档属于什么类型。Servlet默认为 text/plain,但通常需要显式地指定为 text/html。由于经常要设置 Content-Type,因此 HttpServletResponse 提供了一个专用的方法 setContentType注意:除了标识类型,这个还负责标识编码。例如 content-type: text/html; charset=UTF-8

Content-disposition:服务器告诉客户端以什么格式打开响应体数据 默认值 in-line:在当前页面内打开,可以设置成 attachment;filename=xxx:以附件形式打开响应体(就是文件下载)

Content-Encoding:响应头的编码(Encode)。只有在解码之后才可以得到 Content-Type 指定的内容类型。

Content-Length:表示内容长度。

Date:当前的 GMT 时间,例如,Date:Mon,31Dec200104:25:57GMT。Date 描述的时间表示世界标准时,换算成地时间,需要知道用户所在的时区。你可以用 setDateHeader 来设置这个头以避免转换时间格式的麻烦。

Expires:告诉浏览器把回送的资源缓存多长时间,-1 或 0 则是不缓存。

Refresh:告诉浏览器隔多久刷新一次,以秒计。

Transfer-Encoding:告诉浏览器数据的传送格式。

Server:服务器通过这个头告诉浏览器服务器的类型。Server 响应头包含处理请求的原始服务器的软件信息。 包含多个产品标识和注释,产品标识一般按照重要性排序。一般不设置这个值,而是由Web服务器自己设置

Set-Cookie:设置和页面关联的Cookie。


响应空行

作用同上面的请求空行

它的作用是通过一个空行,告诉服务器响应头部到此为止。

响应体

响应体返回回去的内容,如果是纯数据就是返回纯数据,如果请求的是 HTML 页面,那么返回的就是 HTML 代码,如果是 JS 就是 JS 代码,如此之类。

响应报文如下

① 报文协议及版本; ② 状态码及状态描述; ③ 响应报文头,也是由多个属性组成; ④ 响应报文体,即我们真正要的“干货”。

(注意!Content-Type除了标识类型,这个还负责标识编码。

例如:content-type: text/html; charset=UTF-8

分块传输

在学习 Fiddler 抓包的时候注意到一件奇怪的事情,微软的更新下载包时发送了很多一样的 HTTP 请求 image.png

实际上这个就是分块传输

HTTP/1.1 引入了管道化和分块编码。分块编码允许先发送消息体的一部分,当其余的部分可用时再接着发。这时HTTP消息体被分成多个块,客户端可以在完整收到所有数据之前就开始处理这些分块的内容(服务端也可以收到分块请求)。这个技术常用于数据长度动态生成的场景,预先不知道总数据长度。

警告

分块编码和管道化都有队头阻塞(HOL)的问题——在队列首部的消息会阻塞后面消息的发送,且管道化在实际应用中并没有得到很好的支持。

HTTP1.1 协议(RFC2616)开始支持获取文件的部分内容,这为并行下载以及断点续传提供了技术支持。

它通过在 Header 里两个参数实现的,客户端发请求时对应的是 Range ,服务器端响应时对应的是 Content-Range

客户端 Range

用于请求头中,指定第一个字节的位置和最后一个字节的位置,一般格式:

Range:(unit=first byte pos)-[last byte pos]

Range 头部的格式有以下几种情况:

Range: bytes=0-499 表示第 0-499 字节范围的内容 
Range: bytes=500-999 表示第 500-999 字节范围的内容
Range: bytes=-500 表示最后 500 字节的内容
Range: bytes=500- 表示从第 500 字节开始到文件结束部分的内容
Range: bytes=0-0,-1 表示第一个和最后一个字节
Range: bytes=500-600,601-999 同时指定几个范围

服务端 Content-Range

用于响应头中,在发出带 Range 的请求后,服务器会在 Content-Range 头部返回当前接受的范围和文件总大小。一般格式:

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
# 0-499 是指当前发送的数据的范围,而 22400 则是文件的总大小。
Content-Range: bytes 0-499/22400

返回的响应头内容也不同:

HTTP/1.1 200 Ok(不使用断点续传方式) 
HTTP/1.1 206 Partial Content(使用断点续传方式)

管道化

危险

管道化并非所有的服务端都支持,而且随着 HTTP2 的到来,这个特性也逐步被废弃了,这里了解一下就好,HTTP2 的二进制分帧也是参考这个管道化改进的(中间有个过渡协议 SPDY)

在长连接的基础上,HTTP1.1进一步地支持在持久连接上使用管道化(pipelining)特性,这是相对于keep-alive连接的又一性能优化。在相应到达之前,可以将多条请求放入队列,当第一条请求发往服务器的时候,第二第三条请求也可以开始发送了,不用等到第一条请求响应回来,在高延时网络条件下,这样做可以降低网络的环回时间,提高性能。

非管道化与管道化的区别示意:

References